package org.rcgwt.generators;

import java.beans.PropertyDescriptor;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.beanutils.PropertyUtils;
import org.rcgwt.client.model.BeanProperty;
import org.rcgwt.client.model.IsReflected;
import org.rcgwt.client.model.Reflected;
import org.rcgwt.client.model.ReflectedFactory;
import org.rcgwt.client.model.BeanProperty.Access;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

/**
 * Generator of {@link Reflected} wrapper from {@link IsReflected} bean<br/>
 * 
 * 
 * @author Didier MOISE
 */
public class ReflectedGenerator extends Generator {

    private static final String FACTORY_PACKAGE = "org.rcgwt.client.model";

    private static final String FACTORY_NAME = "ReflectedFactoryImpl";

    private TypeOracle typeOracle;

    /** GWT logger */
    private TreeLogger logger;

    private GeneratorContext context;

    /**
     * Generate the reflection factory implementation.
     */
    @Override
    public String generate(TreeLogger logger, GeneratorContext context,
            String typeName) throws UnableToCompleteException {

        typeOracle = context.getTypeOracle();
        assert (typeOracle != null);

        this.logger = logger;
        this.context = context;

        /** List of reflected beans */
        List<JClassType> reflectedBeans = listReflectedBeans(typeOracle);

        // Class composer
        ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
                FACTORY_PACKAGE, FACTORY_NAME);
        composer.setSuperclass(ReflectedFactory.class.getCanonicalName());
        composer.addImport(ReflectedFactory.class.getName());
        composer.addImport(BeanProperty.class.getName());

        // Factory PrintWriter
        PrintWriter pw = context.tryCreate(logger, FACTORY_PACKAGE,
                FACTORY_NAME);

        // Generate the factory
        if (pw != null) {
            // Source writer
            SourceWriter sw = composer.createSourceWriter(context, pw);

            sw.println("public <T> Reflected<T> createReflected(T obj) {");
            sw.println("  String objClassName=obj.getClass().getName();");
            for (JClassType reflectedType : reflectedBeans) {
                // Create the reflected class
                String reflectedClassName = generateReflectedWrapper(reflectedType);

                // Add it to the "createReflected" method
                String beanClassFullName = reflectedType.getPackage().getName()
                        + "." + reflectedType.getName();
                sw.println("if (objClassName.equals(\"" + beanClassFullName
                        + "\"))  {");
                sw.println("	return (Reflected<T>) new " + reflectedClassName
                        + "((" + beanClassFullName + ")obj);");
                sw.println("}");
            }

            sw.println("return null;");
            sw.println("}");

            sw.println("public BeanProperty[] getBeanProperties(String objClassName) {");
            for (JClassType reflectedType : reflectedBeans) {
                // Create the reflected class
                generateBeanProperties(reflectedType, sw, reflectedType
                        .getPackage().getName()
                        + "." + reflectedType.getName());
            }

            sw.println("return null;");

            sw.println("}");

            sw.commit(logger);

        }

        return composer.getCreatedClassName();
    }

    private List<JClassType> listReflectedBeans(TypeOracle typeOracle) {

        Set<JClassType> reflectedBeans = new HashSet<JClassType>();

        /*
         * JClassType isReflectedClassType =
         * typeOracle.findType(IsReflected.class .getName());
         * 
         * JClassType[] classTypes = typeOracle.getTypes(); for (JClassType
         * classType : classTypes) { if (!classType.equals(isReflectedClassType)
         * && isReflectedClassType.isAssignableFrom(classType)) {
         * reflectedBeans.add(classType); } }
         */

        Set<JPackage> packages = new HashSet<JPackage>(Arrays.asList(typeOracle
                .getPackages()));
        filterPackages(packages);

        for (JPackage jp : packages) {
            findReflectedAnnotations(reflectedBeans, jp);
        }

        return new ArrayList<JClassType>(reflectedBeans);
    }

    private void findReflectedAnnotations(Set<JClassType> reflectedBeans,
            JPackage jp) {

        for (JClassType cl : jp.getTypes()) {
            org.rcgwt.client.model.IsReflected annotation = cl
                    .getAnnotation(org.rcgwt.client.model.IsReflected.class);
            if (annotation == null) {
                continue;
            }

            reflectedBeans.add(cl);
        }

    }

    private void filterPackages(Set<JPackage> packages) {
        for (Iterator<JPackage> it = packages.iterator(); it.hasNext();) {
            String name = it.next().getName();

            if (name.startsWith("org.rcgwt.client")) {
                it.remove();
                continue;
            }

            if (name.startsWith("java")) {
                it.remove();
                continue;
            }

            if (name.startsWith("sun")) {
                it.remove();
                continue;
            }

            if (name.startsWith("com.google")) {
                it.remove();
                continue;
            }

        }
    }

    /**
     * Generate the Reflected wrapper class.
     */
    public String generateReflectedWrapper(JClassType reflectedType)
            throws UnableToCompleteException {

        // Generate the name and the package of the reflected class
        String beanClassFullName = reflectedType.getPackage().getName() + "."
                + reflectedType.getName();
        String reflectedPackage = reflectedType.getPackage().getName();
        String reflectedName = "Reflected_" + reflectedType.getName();

        // Initialize the source writer
        SourceWriter src = initReflectedClass(reflectedType, reflectedName,
                reflectedPackage, beanClassFullName);
        if (src != null) {

            src.println("public " + reflectedName + "("
                    + reflectedType.getName() + " bean) {");
            src.println("  super(bean);");
            src.println("}");

            // Generate the source code of the reflected class
            generateGettersAndSettersCallers(reflectedType, src,
                    beanClassFullName);

            // Finalize the source writer
            finalizeReflectedClass(src);
        }

        // Return the reflected class full name
        return reflectedPackage + "." + reflectedName;
    }

    /**
     * Generate the source code of the Reflected Wrapper
     * 
     * @param bean
     * @param src
     */
    private void generateBeanProperties(JClassType bean, SourceWriter sw,
            String beanClassName) throws UnableToCompleteException {

        // Find the bean class
        Class< ? > beanClass;
        String jBeanClass = bean.getPackage().getName() + "." + bean.getName();
        try {
            beanClass = Class.forName(jBeanClass);
        } catch (Exception e) {
            logger.log(TreeLogger.ERROR, "Unable to find class  '" + jBeanClass
                    + "'", null);
            throw new UnableToCompleteException();
        }

        // Add it to the "createReflected" method
        sw.println("if (objClassName.equals(\"" + jBeanClass + "\"))  {");

        sw.println("  return new BeanProperty[] {");

        boolean first = true;

        // Describe the bean properties
        PropertyDescriptor[] properties = PropertyUtils
                .getPropertyDescriptors(beanClass);
        for (PropertyDescriptor p : properties) {

            Method readMethod = p.getReadMethod();

            String name = p.getName();
            if (name.equals("class")) { // doesn't use this inner
                // property
                continue;
            }

            if (readMethod == null) { // If the property cannot be read
                continue;
            }

            String type = validatePropertyTypeForCast(p.getPropertyType()
                    .getName());
            if (type == null) {
                continue;
            }

            if (first) {
                first = false;
            } else {
                sw.println(",");
            }

            String accessType = (p.getWriteMethod() != null) ? "READ_WRITE"
                    : "READ_ONLY";

            sw.print("    new BeanProperty(\"" + name + "\", \"" + type
                    + "\", BeanProperty.Access." + accessType + ")");
        }

        sw.println("  };");

        sw.println("}");
    }

    /**
     * Generate the source code of the Reflected Wrapper
     * 
     * @param bean
     * @param src
     */
    private void generateGettersAndSettersCallers(JClassType bean,
            SourceWriter src, String beanClassName)
            throws UnableToCompleteException {

        // Find the bean class
        Class< ? > beanClass = null;
        String jBeanClass = bean.getPackage().getName() + "." + bean.getName();
        try {
            beanClass = Class.forName(jBeanClass);
        } catch (Exception e) {
            logger.log(TreeLogger.ERROR, "Unable to find class  '" + jBeanClass
                    + "'", null);
            throw new UnableToCompleteException();
        }

        // Describe the bean properties
        PropertyDescriptor[] properties = PropertyUtils
                .getPropertyDescriptors(beanClass);

        // Generate getters
        src.println("protected Object callGetter(" + beanClassName
                + " bean, String property) throws ReflectionException {");
        for (PropertyDescriptor p : properties) {

            Method readMethod = p.getReadMethod();

            String name = p.getName();
            if (name.equals("class")) // doesn't use this inner property
                continue;
            if (readMethod == null) // If the property cannot be read
                continue;

            // Call the getter on the bean
            src.println("if (property.equals(\"" + name + "\"))");
            src.println("return ((" + beanClassName + ")bean)."
                    + readMethod.getName() + "();");
        }
        src
                .println("throw new ReflectionException(\"The property cannot be get\");");
        src.println("}");

        // Generate setters
        src
                .println("protected void callSetter("
                        + beanClassName
                        + " bean, String property, Object value) throws ReflectionException {");
        for (PropertyDescriptor p : properties) {

            Method writeMethod = p.getWriteMethod();
            String name = p.getName();
            if (name.equals("class")) // doesn't use this inner property
                continue;
            if (writeMethod == null) // If the property cannot be write
                continue;

            // Call the setter on the bean
            src.println("if (property.equals(\"" + name + "\")) {");
            String propertyBasedClass = p.getWriteMethod().getDeclaringClass()
                    .getName();
            src
                    .println("(("
                            + propertyBasedClass
                            + ")bean)."
                            + writeMethod.getName()
                            + "(("
                            + validatePropertyTypeForCast(p.getPropertyType()
                                    .getName()) + ")value);");
            src.println("return;");
            src.println("}");

        }
        src
                .println("throw new ReflectionException(\"The property cannot be set\");");
        src.println("}");
    }

    /**
     * Initialize the creation of the reflected class
     */
    private SourceWriter initReflectedClass(JClassType requestedBeanClass,
            String reflectedName, String reflectedPackage,
            String reflectedClassFullName) {

        // Create the source writer and return it
        PrintWriter printWriter = context.tryCreate(logger, reflectedPackage,
                reflectedName);
        if (printWriter == null) {
            return null;
        }
        ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
                reflectedPackage, reflectedName);
        String superClassNameFull = Reflected.class.getName() + "<"
                + reflectedClassFullName + ">";
        composerFactory.setSuperclass(superClassNameFull);
        composerFactory.addImport(FACTORY_PACKAGE + ".ReflectionException");
        return composerFactory.createSourceWriter(context, printWriter);
    }

    /**
     * Finalize the creation of the reflected class
     * 
     * @param sourceWriter
     */
    private void finalizeReflectedClass(SourceWriter sourceWriter) {
        sourceWriter.commit(logger);
    }

    private String validatePropertyTypeForCast(String value) {
        if (value.equals("boolean")) {
            return "Boolean";
        }
        if (value.equals("int")) {
            return "Integer";
        }
        if (value.equals("double")) {
            return "Double";
        }
        if (value.equals("float")) {
            return "Float";
        }
        if (value.equals("char")) {
            return "Character";
        }
        if (value.equals("byte")) {
            return "Byte";
        }
        if (value.equals("short")) {
            return "Short";
        }
        if (value.equals("long")) {
            return "Long";
        }
        return value;
    }
}
